package main

import (
	"bufio"
	"fmt"
	"log"
	"io"
	"os"
	"os/exec"
	"math/rand"
	"sort"
	"strings"
)

type Recipe struct {
	name string
	tags []string
	image string
	code string
	description string
}

type DB struct {
	cfg *Config
	recipes []Recipe
}


func initDB(cfg *Config) (*DB, error) {
	if err := checkCfgDir(cfg); err != nil { 
		return nil, err
	}
	
	db := &DB{ cfg: cfg }
	db.read()

	return db, nil
}


func checkCfgDir(cfg *Config) error {
	fi, err := os.Stat(cfg.dbPath)
	if err != nil {
		if os.IsNotExist(err) {
			if os.Mkdir(cfg.dbPath, 0777) != nil {
				return err
			}
		} else {
			return err
		}
	} else if !fi.IsDir() {
		log.Fatal("config path is not a directory")
	}
	return nil
}


func (db *DB) read() error {
	// cleanup current
	db.recipes = make([]Recipe, 0, 100)

	// open file
	fi, err := os.OpenFile(fmt.Sprintf("%s/recipes.txt", db.cfg.dbPath),
			os.O_RDONLY | os.O_CREATE, 0600)
	if err != nil {
		return err 
	}
	defer fi.Close()
	buf := bufio.NewReader(fi)

	// load recipe file
	cont := true
	for cont {
		recipe, c, err := parse(buf)
		cont = c
		if err != nil {
			return err
		}
		db.recipes = append(db.recipes, *recipe)
	}

	return nil
}


func (db *DB) save() error {
	// backup file
	cmd := exec.Command("cp", "-f", fmt.Sprintf("%s/recipes.txt", db.cfg.dbPath),
			fmt.Sprintf("%s/recipes.txt~", db.cfg.dbPath))
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		return err
	}

	// open database
	f, err := os.Create(fmt.Sprintf("%s/recipes.txt", db.cfg.dbPath))
	if err != nil {
		return err
	}
	defer f.Close()

	// save recipes
	for _, recipe := range db.recipes {
		s := fmt.Sprintf("Code: %s\nName: %s\nTags: %s\nImage: %s\nDescription:\n%s.\n",
				recipe.code,
				recipe.name, strings.Join(recipe.tags, ","),
				recipe.image, recipe.description)
		_, err = f.WriteString(s)
		if err != nil {
			return err
		}
	}

	return nil
}


func (db *DB) add(file string) (*Recipe, error) {
	// open file
	fi, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer fi.Close()
	buf := bufio.NewReader(fi)

	// parse file
	recipe, _, err := parse(buf)
	if err != nil {
		return nil, err
	}

	// create code
	recipe.code = db.newCode()

	// add to DB
	db.recipes = append(db.recipes, *recipe)
	err = db.save()
	if err != nil {
		return nil, err
	}

	return recipe, nil
}


func parse(buf *bufio.Reader) (*Recipe, bool, error) {
	var err error
	
	// read file
	r := new(Recipe)
	err = nil
	description := false
	for err != io.EOF {
		var s string
		s, err = buf.ReadString('\n')
		s = strings.Trim(s, "\n")
		s = strings.Trim(s, "\r")
		if err != nil && err != io.EOF {
			return nil, false, err
		}
		if s == "." {
			return r, true, nil
		}
		if !description {
			if n := strings.Index(s, "Name:"); n == 0 {
				r.name = strings.Trim(s[5:], " ")
			} else if n := strings.Index(s, "Tags:"); n == 0 {
				tags := strings.Split(s[5:], ",")
				for _, tag := range tags {
					r.tags = append(r.tags, 
							strings.Trim(tag, " "))
				}
			} else if n := strings.Index(s, "Image:"); n == 0 {
				r.image = strings.Trim(s[6:], " ")
			} else if n := strings.Index(s, "Code:"); n == 0 {
				r.code = strings.Trim(s[5:], " ")
			} else if n := strings.Index(s, "Description:"); n == 0 {
				description = true
			} else {
				return nil, false, &InvalidSyntax{}
			}
		} else {
			r.description += s + "\n"
		}
	}

	return r, false, nil // EOF
}


func (db *DB) newCode() string {
	sameCode := true
	code := ""
	for sameCode {
		code = ""
		sameCode = false
		for i := 0; i<4; i++ {
			c := (rand.Int() % 27) + 'a'
			code = fmt.Sprintf("%s%c", code, c)
		}
		for _, recipe := range db.recipes {
			if recipe.code == code {
				sameCode = true
			}
		}
	}
	return code
}


func (db* DB) tags() ([]string, map[string] int) {
	m := make(map[string] int)
	tags := make(sort.StringSlice, 0)
	for _, recipe := range db.recipes {
		for _, tag := range recipe.tags {
			// add to map
			m[tag]++
			// add to array
			found := false
			for _, t := range tags {
				if t == tag {
					found = true
				}
			}
			if !found {
				tags = append(tags, tag)
			}
		}
	}
	tags.Sort()
	return tags, m
}


type InvalidSyntax struct {}
func (e *InvalidSyntax) Error() string { return "Invalid syntax." }
